﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Diagnostics.Contracts;

namespace FoundationLib.Common.Calendar
{
	/// <summary>
	/// Represents a month of a calendar year.
	/// </summary>
	public class MonthOfYear : IComparable<MonthOfYear>
	{
		/// <summary>
		/// The date of the first day in this month
		/// </summary>
		private DateTime startDate;
		/// <summary>
		/// The date of the last day in this month
		/// </summary>
		private DateTime endDate;
		/// <summary>
		/// The culture to be used
		/// </summary>
		private CultureInfo culture;
		/// <summary>
		/// A set of calendar weeks contained in this month
		/// </summary>
		private List<WeekOfYear> weeks;


		/// <summary>
		/// Creates a new <see cref="MonthOfYear"/> instance.
		/// </summary>
		private MonthOfYear()
		{
			this.culture = CultureInfo.CurrentCulture;
			this.weeks = new List<WeekOfYear>();
		}

		/// <summary>
		/// Created a new <see cref="MonthOfYear"/> instance.
		/// </summary>
		/// <param name="date">A date specifying the month</param>
		public MonthOfYear(DateTime date)
			: this()
		{
			int year = date.Year;
			int month = date.Month;
			int day = 1;

			this.startDate = new DateTime(year, month, day, 0, 0, 0);
			this.endDate = new DateTime(year, month, DateTime.DaysInMonth(year, month), 0, 0, 0);
		}

		/// <summary>
		/// Created a new <see cref="MonthOfYear"/> instance.
		/// </summary>
		/// <param name="date">A date specifying the month</param>
		/// <param name="culture">The culture to be used</param>
		public MonthOfYear(DateTime date, CultureInfo culture)
			: this(date)
		{
			Contract.Requires(culture != null);
			this.culture = culture;
		}

		/// <summary>
		/// The object invariant, for internal use only.
		/// </summary>
		[ContractInvariantMethod]
		private void ObjectInvariant()
		{
			Contract.Invariant(this.culture != null);
			Contract.Invariant(this.startDate < this.endDate);
			Contract.Invariant(this.weeks != null);
		}

		/// <summary>
		/// Gets the date of the first day of this month.
		/// </summary>
		public DateTime StartDate
		{
			get
			{
				return this.startDate;
			}
		}

		/// <summary>
		/// Gets the date of the last day of this month.
		/// </summary>
		public DateTime EndDate
		{
			get
			{
				return this.endDate;
			}
		}

		/// <summary>
		/// Gets a numeric representation of this month.
		/// </summary>
		public int Month
		{
			get
			{
				return this.startDate.Month;
			}
		}

		/// <summary>
		/// Gets this month's calendar year.
		/// </summary>
		public int Year
		{
			get
			{
				return this.startDate.Year;
			}
		}

		/// <summary>
		/// Gets the number of days contained in this month.
		/// </summary>
		public int DaysInMonth
		{
			get
			{
				return DateTime.DaysInMonth(this.startDate.Year, this.startDate.Month);
			}
		}

		/// <summary>
		/// Gets the number of weeks spanned by this month.
		/// </summary>
		public int WeeksInMonth
		{
			get
			{
				return (this.Weeks.Count());
			}
		}

		/// <summary>
		/// Gets the weeks spanned by this month.
		/// </summary>
		public WeekOfYear[] Weeks
		{
			get
			{
				if (this.weeks.Count > 0)
					return this.weeks.ToArray();

				WeekOfYear week = WeekOfYear.FromDate(this.startDate);
				while (week.StartDate <= this.endDate)
				{
					this.weeks.Add(week);
					week = week.AddWeeks(1);
				}

				return this.weeks.ToArray();
			}
		}

		/// <summary>
		/// Adds the given time span to this month and returns the result.
		/// </summary>
		/// <param name="value">The time span to add</param>
		/// <returns>The resulting month of year</returns>
		public MonthOfYear Add(TimeSpan value)
		{
			return (new MonthOfYear(this.startDate.Add(value), this.culture));
		}

		/// <summary>
		/// Adds the given number of months to this month and returns the result.
		/// </summary>
		/// <param name="value">The number of months to add</param>
		/// <returns>The resulting month of year</returns>
		public MonthOfYear AddMonths(int value)
		{
			return (new MonthOfYear(this.startDate.AddMonths(value), this.culture));
		}

		/// <summary>
		/// Gets a <see cref="MonthOfYear"/> instance representing the current month.
		/// </summary>
		public static MonthOfYear ThisMonth
		{
			get
			{
				return (new MonthOfYear(DateTime.Now));
			}
		}

		/// <summary>
		/// Gets a <see cref="MonthOfYear"/> instance representing next month.
		/// </summary>
		public static MonthOfYear NextMonth
		{
			get
			{
				return (new MonthOfYear(DateTime.Today.AddMonths(1)));
			}
		}

		/// <summary>
		/// Gets a <see cref="MonthOfYear"/> instance representing the previous month.
		/// </summary>
		public static MonthOfYear LastMonth
		{
			get
			{
				return (new MonthOfYear(DateTime.Today.AddMonths(-1)));
			}
		}

		/// <summary>
		/// Checks two <see cref="MonthOfYear"/> instances for equality.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if both instances are equal, false otherwise</returns>
		public static bool operator ==(MonthOfYear first, MonthOfYear second)
		{
			if (first == null || second == null)
				return false;
			return (first.Equals(second));
		}

		/// <summary>
		/// Checks two <see cref="MonthOfYear"/> instances for inequality.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if the instances are not equal, false otherwise</returns>
		public static bool operator !=(MonthOfYear first, MonthOfYear second)
		{
			return (!(first == second));
		}

		/// <summary>
		/// Checks whether <paramref name="first"/> is greater than <paramref name="second"/>.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is greater than <paramref name="second"/>, false otherwise</returns>
		public static bool operator >(MonthOfYear first, MonthOfYear second)
		{
			return (first.CompareTo(second) > 0);
		}

		/// <summary>
		/// Checks whether <paramref name="first"/> is greater than or equal to <paramref name="second"/>.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is greater than or equal to <paramref name="second"/>, false otherwise</returns>
		public static bool operator >=(MonthOfYear first, MonthOfYear second)
		{
			return (first > second || first == second);
		}

		/// <summary>
		/// Checks whether <paramref name="first"/> is less than <paramref name="second"/>.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is less than <paramref name="second"/>, false otherwise</returns>
		public static bool operator <(MonthOfYear first, MonthOfYear second)
		{
			return (first.CompareTo(second) < 0);
		}

		/// <summary>
		/// Checks whether <paramref name="first"/> is less than or equal to <paramref name="second"/>.
		/// </summary>
		/// <param name="first">The first instance</param>
		/// <param name="second">The second instance</param>
		/// <returns>True if <paramref name="first"/> is less than or equal to <paramref name="second"/>, false otherwise</returns>
		public static bool operator <=(MonthOfYear first, MonthOfYear second)
		{
			return (first < second || first == second);
		}

		/// <see cref="Object.Equals(Object)"/>
		public override bool Equals(object obj)
		{
			MonthOfYear month = obj as MonthOfYear;
			if (obj == null)
				return false;
			return (month.StartDate.Equals(this.startDate));
		}

		/// <see cref="Object.GetHashCode()"/>
		public override int GetHashCode()
		{
			return this.startDate.GetHashCode();
		}

		/// <see cref="Object.ToString()"/>
		public override string ToString()
		{
			return (this.startDate.ToString("MMMM yyyy"));
		}

		/// <see cref="IComparable{T}.CompareTo(T)"/>
		public int CompareTo(MonthOfYear other)
		{
			if (other == null)
				throw new ArgumentNullException("other");

			if (other.StartDate == this.StartDate)
				return 0;
			else if (other.StartDate < this.StartDate)
				return 1;
			else
				return -1;
		}
	}
}
